#!/usr/bin/env bash
# Copyright (c) 2024 maminjie <canpool@163.com>
# SPDX-License-Identifier: MulanPSL-2.0

CUR_DIR=$(pwd)
WORK_DIR="$CUR_DIR/oe_diff_workspace"
YUM_DIR="$WORK_DIR/yum"
YUM_CONFIG_FILE="$YUM_DIR/yum.conf"

RPM_DIR="$WORK_DIR/rpm"

A_REPOS=""
B_REPOS=""
HAS_DIST_FLAG=1
OUT_DIFF_FLAG=0
RPM_DOWNLOAD_FLAG=0

usage() {
printf "diff rpm packages between the two systems based on dnf repository

usage:
    bash oe_diff.sh [-h] [-a REPOSITORY [-a ...]] [-b REPOSITORY [-b ...]] [-u] [-t]

optional arguments:
    -a REPOSITORY  A dnf repository address link (can be specified multiple times)
    -b REPOSITORY  B dnf repository address link (can be specified multiple times)
    -u             the flag that only output difference items
    -t             the flag that rpm name does not contain the dist suffix
    -d             the flag that download new rpm
    -h             show the help message and exit

examples:
    bash oe_diff.sh -a A_ADDR -b B_ADDR
    bash oe_diff.sh -a A_ADDR1 -a A_ADDR2 -b B_ADDR1 -b B_ADDR2
\n"
}

log_error() {
    echo "ERROR: $1"
}

parse_cmdline() {
    if [ $# -eq 0 ]; then
        usage; exit 0
    fi

    # parse cmd line arguments
    while getopts ":a:b:utdh" opt; do
        case "$opt" in
            a)
                if [ -z "$A_REPOS" ]; then
                    A_REPOS="$OPTARG"
                else
                    A_REPOS="$A_REPOS $OPTARG"
                fi
            ;;
            b)
                if [ -z "$B_REPOS" ]; then
                    B_REPOS="$OPTARG"
                else
                    B_REPOS="$B_REPOS $OPTARG"
                fi
            ;;
            u)
                OUT_DIFF_FLAG=1
            ;;
            t)
                HAS_DIST_FLAG=0
            ;;
            d)
                RPM_DOWNLOAD_FLAG=1
            ;;
            h)
                usage; exit 0
            ;;
            ?)
                echo "please check the params."; usage; return 1
            ;;
        esac
    done
    if [[ -z "$A_REPOS" || -z "$B_REPOS" ]]; then
        echo "invalid REPOSITORY arguments"; return 1
    fi
    return 0
}

check_requires() {
    local ret=0

    for c in repoquery rpmdev-vercmp yumdownloader; do
        command -v $c >/dev/null
        if [ $? -ne 0 ]; then
            echo "command $c not found"
            ret=1
        fi
    done

    return $ret
}

clean() {
    cd "$CUR_DIR" || :
    [[ -d "${WORK_DIR}" ]] && rm -rf "${WORK_DIR}"
}

create_yum_repo() {
    local yum_dir="$YUM_DIR"
    local yum_config_file="$YUM_CONFIG_FILE"
    local yum_repo_dir="${yum_dir}/yum.repos.d"
    local yum_repo_file="${yum_repo_dir}/os.repo"

    mkdir -p "$yum_repo_dir"

    cat > "${yum_config_file}" <<EOF
[main]
gpgcheck=1
installonly_limit=3
clean_requirements_on_remove=True
best=True
skip_if_unavailable=False
reposdir=${yum_repo_dir}
EOF

    local i=1
    for repo in $A_REPOS; do
        cat >> "${yum_repo_file}" <<EOF
[a_repo_$i]
name=a_repo_$i
baseurl=$repo
enabled=1
gpgcheck=0

EOF
        ((i++))
    done

    i=1
    for repo in $B_REPOS; do
        cat >> "${yum_repo_file}" <<EOF
[b_repo_$i]
name=b_repo_$i
baseurl=$repo
enabled=1
gpgcheck=0

EOF
        ((i++))
    done
}


# get_repo_args repo_ids
get_repo_args() {
    local repo_args=""
    for r in $1; do
        repo_args="$repo_args --repo $r"
    done
    echo "$repo_args"
}

# get_packages repo_ids
get_packages() {
    local repo_ids="$1"
    local repo_args=""
    for r in $repo_ids; do
        repo_args="$repo_args --repo $r"
    done
    repoquery -c "$YUM_CONFIG_FILE" -q --qf="%{name},%{epoch}:%{version}-%{release},%{arch}" --all --latest-limit=1 $repo_args
}

get_binary_packages() {
    echo "$1" | grep -v ",src$" | grep -v "debuginfo," | grep -v "debugsource,"
}

get_source_packages() {
    echo "$1" | grep ",src$"
}

get_debug_packages() {
    echo "$1" | grep -E "(debuginfo|debugsource),"
}


# do_diff type a_pkgs b_pkgs
do_diff() {
    # type,pkg_name,op,version_change,a_rpm_name,a_ver_full,b_rpm_name,b_ver_full
    # equal updated degraded added removed unknown
    local t=$1
    local a_pkgs=($2)
    local b_pkgs=($3)
    local a_len=${#a_pkgs[@]}
    local b_len=${#b_pkgs[@]}
    local i=0 j=0
    while true; do
        local new_flag=0
        local pkg_name=""
        local item=""
        if [ $i -lt $a_len ]; then
            local a_pkg=${a_pkgs[$i]}
            local a_name=${a_pkg%%,*}
            local a_arch=${a_pkg##*,}
            local a_ver_full=$(echo "$a_pkg" | cut -d , -f 2)
            local a_ver_cmp=$a_ver_full
            local a_epoch=${a_ver_full%%:*}
            local a_ver_str=${a_ver_full##*:}
            local a_ver=$a_ver_str
            if [ $HAS_DIST_FLAG -eq 1 ]; then
                a_ver=${a_ver_str%.*}
                a_ver_cmp=${a_ver_full%.*}
            fi
            local a_rpm_name="$a_name-$a_ver_str.$a_arch.rpm"
            pkg_name="$a_name"
            if [ $j -lt $b_len ]; then
                local b_pkg=${b_pkgs[$j]}
                local b_name=${b_pkg%%,*}
                local b_arch=${b_pkg##*,}
                local b_ver_full=$(echo "$b_pkg" | cut -d , -f 2)
                local b_ver_cmp=$b_ver_full
                local b_epoch=${b_ver_full%%:*}
                local b_ver_str=${b_ver_full##*:}
                local b_ver=$b_ver_str
                if [ $HAS_DIST_FLAG -eq 1 ]; then
                    b_ver=${b_ver_str%.*}
                    b_ver_cmp=${b_ver_full%.*}
                fi
                local b_rpm_name="$b_name-$b_ver_str.$b_arch.rpm"
                if [[ "$a_name" = "$b_name" ]]; then
                    rpmdev-vercmp $a_ver_cmp $b_ver_cmp > /dev/null
                    local ret=$?
                    local op="unknown"
                    local ver_change="$a_ver -> $b_ver"
                    if [ $ret -eq 0 ]; then
                        op="equal"
                        ver_change="$a_ver"
                    elif [ $ret -eq 11 ]; then
                        # a is newer than b
                        op="degraded"
                    elif [ $ret -eq 12 ]; then
                        # a is older than b
                        op="updated"
                        new_flag=1
                    fi
                    ((i++))
                    ((j++))
                    if [[ $OUT_DIFF_FLAG -eq 1 && "$op" = "equal" ]]; then
                        continue
                    else
                        item="$t,$a_name,$op,$ver_change,$a_rpm_name,$a_ver_full,$b_rpm_name,$b_ver_full"
                    fi
                elif [[ "$a_name" < "$b_name" ]]; then
                    item="$t,$a_name,removed,$a_ver,$a_rpm_name,$a_ver_full,,"
                    ((i++))
                else
                    new_flag=1
                    pkg_name="$b_name"
                    item="$t,$b_name,added,$b_ver,,,$b_rpm_name,$b_ver_full"
                    ((j++))
                fi
            else
                item="$t,$a_name,removed,$a_ver,$a_rpm_name,$a_ver_full,,"
                ((i++))
            fi
        else
            if [ $j -lt $b_len ]; then
                local b_pkg=${b_pkgs[$j]}
                local b_name=${b_pkg%%,*}
                local b_arch=${b_pkg##*,}
                local b_ver_full=$(echo "$b_pkg" | cut -d , -f 2)
                local b_epoch=${b_ver_full%%:*}
                local b_ver_str=${b_ver_full##*:}
                local b_ver=$b_ver_str
                if [ $HAS_DIST_FLAG -eq 1 ]; then
                    b_ver=${b_ver_str%.*}
                fi
                local b_rpm_name="$b_name-$b_ver_str.$b_arch.rpm"
                new_flag=1
                pkg_name="$b_name"
                item="$t,$b_name,,,added,$b_rpm_name,$b_ver_full,$b_ver"
                ((j++))
            else
                break
            fi
        fi
        if [[ $RPM_DOWNLOAD_FLAG -eq 1 ]]; then
            if [[ $new_flag -eq 1 && -n "$pkg_name" ]]; then
                echo "$item" | tee -a "$WORK_DIR/diff.csv"
                local rpm_list_file="$RPM_DIR/rpmlist.$t"
                echo "$pkg_name" >> $rpm_list_file
            fi
        else
            echo "$item"
        fi
    done
}


# download_rpm repo_ids
download_rpm() {
    local types="binary source debug"
    for t in $types; do
        local rpm_list_file="$RPM_DIR/rpmlist.$t"
        if [ -f "$rpm_list_file" ]; then
            local rpm_dir="$RPM_DIR/$t"
            local args=""
            if [ "$t" = "source" ]; then
                args="--source"
            fi
            mkdir -p $rpm_dir
            yumdownloader -c "$YUM_CONFIG_FILE" $args --destdir $rpm_dir $(get_repo_args "$1") $(cat $rpm_list_file | tr '\n' ' ')
        fi
    done
}


do_main() {
    clean

    mkdir -p "$YUM_DIR"
    if [ $RPM_DOWNLOAD_FLAG -eq 1 ]; then
        mkdir -p "$RPM_DIR"
    fi

    create_yum_repo

    local repo_ids=$(dnf repolist -c $YUM_CONFIG_FILE | grep -v "repo id" | awk '{print $1}')
    local a_repos=$(echo "$repo_ids" | grep a_repo_)
    local b_repos=$(echo "$repo_ids" | grep b_repo_)

    local a_pkgs=$(get_packages "$a_repos")
    local b_pkgs=$(get_packages "$b_repos")

    echo "type,pkg_name,op,version_change,a_rpm_name,a_ver_full,b_rpm_name,b_ver_full"

    local a_bin_pkgs=$(get_binary_packages "$a_pkgs")
    local b_bin_pkgs=$(get_binary_packages "$b_pkgs")

    do_diff "binary" "$a_bin_pkgs" "$b_bin_pkgs"

    local a_src_pkgs=$(get_source_packages "$a_pkgs")
    local b_src_pkgs=$(get_source_packages "$b_pkgs")

    do_diff "source" "$a_src_pkgs" "$b_src_pkgs"

    local a_dbg_pkgs=$(get_debug_packages "$a_pkgs")
    local b_dbg_pkgs=$(get_debug_packages "$b_pkgs")

    do_diff "debug" "$a_dbg_pkgs" "$b_dbg_pkgs"

    if [ $RPM_DOWNLOAD_FLAG -eq 1 ]; then
        download_rpm "$b_repos"
    else
        clean
    fi

    return 0
}

main() {
    parse_cmdline "$@"
    if [ $? -ne 0 ]; then
        log_error "parse params failed, try -h for help"
        return 1
    fi

    check_requires
    if [ $? -ne 0 ]; then
        log_error "check requires failed"
        return 1
    fi

    do_main
    if [ $? -ne 0 ]; then
        return 1
    fi

    return 0
}

main "$@"
